/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
**/


package com.chasekernan.hxnova.core.minerals;

import com.chasekernan.hxnova.core.stars.Concentrations; 
import haxe.xml.Check;

/**
    Stores amounts of minerals based upon their type in ints.
**/
class MineralSet {
    
    public var values(default, null) : Array<Int>;
    
    /**
        Defaults all values to 0.
    **/
    public function new() {
        values = new Array<Int>();
        for(i in Concentrations.MINERAL_TYPES) {
            values.push(0);
        }
    }
    
    /**
        Returns the amount of a specific type of mineral.
        Will throw an error if there is no such mineral type.
    **/
    public function getValue(type : String) : Int {
        for(i in 0...Concentrations.MINERAL_TYPES.length) {
            if(Concentrations.MINERAL_TYPES[i] == type) return values[i];
        }
        
        throw "No such mineral type as " + type;
    }
    
    /**
        Sets the amount of a specific type of mineral.
        Will throw an error if there is no such mineral type  or
        the value is less than 0.
    **/
    public function setValue(type : String, value : Int) {
        if (!checkValue(value)) throw "Value is out of bounds: " + value;
        
        for(i in 0...Concentrations.MINERAL_TYPES.length) {
            if(Concentrations.MINERAL_TYPES[i] == type) {
                values[i] = value;
                return;
            }
        }
        
        throw "No such mineral type as " + type;
    }
    
    /**
        Adds the contents of another mineral set to this one and
        returns itself.
    **/
    public function add(minSet : MineralSet) : MineralSet {
        for(i in Concentrations.MINERAL_TYPES) {
            setValue(i, getValue(i) + minSet.getValue(i));
        }
        
        return this;
    }
    
    /**
        Subtracts the contents of another mineral set from this one and
        returns itself.
        Will throw an error if the other set is larger than this one.
    **/
    public function subtract(minSet : MineralSet) : MineralSet {
        if (isLessThanOrEqualTo(minSet)) 
            throw "Other mineral set is too large to subtract.";
        
        for(i in Concentrations.MINERAL_TYPES) {
            setValue(i, getValue(i) - minSet.getValue(i));
        }
        
        return this;
    }
    
    /**
        Scales the current set by a float constant and returns itself.
        Will throw an error if the amount is < 0.
    **/
    public function multiply(amount : Float) : MineralSet {
        if(amount < 0) throw "Amount must be >= 0.";
        
        for(i in Concentrations.MINERAL_TYPES) {
            setValue(i, Std.int(getValue(i) * amount));
        }
        
        return this;
    }
    
    /**
        Divides the current set by a float constant and returns itself.
        Will throw an error if the constant is <= 0.
    **/
    public function divideByInt(amount : Float) : MineralSet {
        if (amount <= 0) throw "Amount must be greater than 0.";
        
        for (i in Concentrations.MINERAL_TYPES) {
            setValue(i , Std.int(getValue(i) / amount));
        }
        
        return this;
    }
    
    /**
        Checks to see if the current value is >= 0.
    **/
    public static function checkValue(value : Int) : Bool{
        return (value >= 0);
    }
    
    public function clone() : MineralSet {
        var c = new MineralSet();
        c.values = values.copy();
        return c;
    }
    
    /**
        If every value in this mineral set is >= the corresponding value
        in the other mineral set, then this returns true.
    **/
    public function isGreaterThanOrEqualTo(m : MineralSet) : Bool {
        for (t in Concentrations.MINERAL_TYPES) {
            if (getValue(t) < m.getValue(t)) return false;
        }
        
        return true;
    }
    
    /**
        If every value in this mineral set is == the corresponding value
        in the other mineral set, then this returns true.
    **/
    public function isEqualTo(m : MineralSet) : Bool {
        for(t in Concentrations.MINERAL_TYPES) {
            if(getValue(t) != m.getValue(t)) return false;
        }
        
        return true;
    }
    
    /**
        If every value in this mineral set is <= the corresponding value
        in the other mineral set, then this returns true.
    **/
    public function isLessThanOrEqualTo(m : MineralSet) : Bool {
        for (t in Concentrations.MINERAL_TYPES) {
            if (getValue(t) > m.getValue(t)) return false;
        }
        return true;
    }
    
    /**
        Returns how many other mineral sets of size [m] are inside this one.
        Used mainly for the ProductionQue and building partial items.
    **/
    public function divideByMinerals(m : MineralSet) : Float {
        var lowest : Float = Math.POSITIVE_INFINITY;
        
        for (t in Concentrations.MINERAL_TYPES) {
            var val : Float = if (m.getValue(t) == 0) Math.POSITIVE_INFINITY 
                              else getValue(t) / (1.0 * m.getValue(t));
            if (val < lowest) lowest = val;
        }
        
        return lowest;
    }
    
    /**
        Returns the mass of the mineral set by adding the amount of 
        each mineral type.
    **/
    public function getMass() : Int {
        var m = 0;
        for(i in this) m += i;
        return m;
    }
    
    public function iterator () {
        return values.iterator();
    }
    
    public static var XML_TITLE = "mineral_set";
    
    public static function getXmlRules() : Rule {
        var nameArray : Array<Rule> = new Array<Rule>();
        
        for(i in Concentrations.MINERAL_TYPES) nameArray.push(Rule.RNode(i, [Att('value')]));
        
        return Rule.RNode(XML_TITLE, [], Rule.RList(nameArray, false));
    }
    
    /**
        Writes a mineral set to xml.
        Sample XML:
            <mineral_set>
                <ironium value="45">
                <boranium value="23">
                <germanium value="0">
            </mineral_set>
    **/
    public static function writeToXml(ms : MineralSet) : Xml {
        var xml = Xml.parse('<' + XML_TITLE + '></' + XML_TITLE + '>')
                .firstChild();
        
        for(i in Concentrations.MINERAL_TYPES) {
            var childXml = Xml.createElement(i);
            childXml.set('value', Std.string(ms.getValue(i)));
            xml.addChild(childXml);
        }
        //check it 
        Check.checkNode(xml, getXmlRules());
        return xml;
    }
    
    /**
        Reads a mineral set from xml.
        Throws an error if there are any mistakes.
    **/
    public static function readFromXml(xml : Xml) : MineralSet {
        try {
            //check type
            Check.checkNode(xml, getXmlRules());
            
            var ms = new MineralSet();
            
            for(i in xml.elements()) {
                ms.setValue(i.nodeName, Std.parseInt(i.get('value')));
            }
            
            return ms;
        } catch (e : Dynamic) {
            throw "Bad mineral set file: " + e;
        }
    }
}

#if unit
import haxe.unit.TestCase;

class MineralSetTest extends haxe.unit.TestCase {
    public var mineralSet : MineralSet;
    
    public function testMineralSet() {
        mineralSet = new MineralSet();
        
        assertEquals(mineralSet.getValue(Concentrations.MINERAL_TYPES[1]), 0);
        
        mineralSet.setValue(Concentrations.MINERAL_TYPES[2], 5);
        assertEquals(mineralSet.getValue(Concentrations.MINERAL_TYPES[2]), 5);
        
        try {
            mineralSet.setValue(Concentrations.MINERAL_TYPES[0], -5);
            assertTrue(false);
        } catch (e : Dynamic) {
            assertTrue(true);
        }
        
        try {
            mineralSet.setValue("gibberish", 3);
            assertTrue(false);
        } catch (e : Dynamic) {
            assertTrue(true);
        }
        
        mineralSet.setValue(Concentrations.MINERAL_TYPES[0], 3);
        mineralSet.setValue(Concentrations.MINERAL_TYPES[1], 4);
        
        assertEquals(mineralSet.getMass(), 12);
        
        var clone = mineralSet.clone();
        assertEquals(clone.getMass(), 12);
        assertTrue(mineralSet.isEqualTo(clone));
        
        mineralSet.add(mineralSet);
        assertEquals(mineralSet.getValue(Concentrations.MINERAL_TYPES[0]), 6);
        assertEquals(mineralSet.getMass(), 24);
        
        mineralSet.subtract(clone);
        assertEquals(mineralSet.getMass(), 12);
        assertEquals(mineralSet.getValue(Concentrations.MINERAL_TYPES[2]), 5);
        
        clone.multiply(4);
        assertEquals(clone.getValue(Concentrations.MINERAL_TYPES[0]), 12);
        assertTrue(mineralSet.isLessThanOrEqualTo(clone));
        assertTrue(clone.isGreaterThanOrEqualTo(mineralSet));
        
        try {
            mineralSet.subtract(clone);
            assertTrue(false);
        } catch (e : Dynamic) {
            //should throw an error if clone > mineralSet
            assertTrue(true);
        }
        
        clone.divideByInt(2);
        assertEquals(clone.getValue(Concentrations.MINERAL_TYPES[1]), 8);
        
        assertTrue(Math.abs(clone.divideByMinerals(mineralSet) - 2) < 0.001);
    }
    
    public function testMineralSetXml() {
        mineralSet = new MineralSet();
        
        var values : Hash<Int> = new Hash<Int>();
        for (minType in Concentrations.MINERAL_TYPES) {
            values.set(minType, Std.random(10000));
            mineralSet.setValue(minType, values.get(minType));
        }
        
        var xml = MineralSet.writeToXml(mineralSet);
        for (element in xml.elements()) {
            assertEquals(values.get(element.nodeName), 
                    Std.parseInt(element.get("value")));
        }
        
        var readValues = MineralSet.readFromXml(xml);
        assertTrue(mineralSet.isEqualTo(readValues));
    }
}

#end